<?php
/**
 * This file is part of the Sphoof framework.
 * Copyright (c) 2010-2011 Sphoof
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code. You can also view the
 * LICENSE file online at http://www.sphoof.nl/new-bsd.txt
 *
 * @category	Sphoof
 * @copyright	Copyright (c) 2010-2011 Sphoof (http://sphoof.nl)
 * @license		http://sphoof.nl/new-bsd.txt	New BSD License
 * @package		Validation
 */

require_once dirname( __FILE__ ) . '/exception.php';

/**
 * An exception that will be thrown when the user calls on an undefined alias.
 *
 * @package Validation
 * @subpackage Exceptions
 */
class SpNoSuchRule extends SpException { }

/**
 * An exception that will be thrown when the user calls a rule without passing
 * all required parameters.
 *
 * @package Validation
 * @subpackage Exceptions
 */
class SpNotEnoughParameters extends SpException { }

/**
 * The validator is a class that can be used to validate values.
 *
 * @package Validation
 */
class SpValidator {
	/**
	 * An array of alias to rule-classname mappings.
	 *
	 * @var Array
	 */
	protected $rules = array(
		'lengthBetween' => 'SpLengthBetweenRule',
		'integer' => 'SpIntegerRule',
		'matches' => 'SpMatchesRule',
		'not' => 'SpNotEqualRule',
		'equals' => 'SpEqualRule'
	);

	/**
	 * An array of set policies.
	 *
	 * @var array
	 */
	protected $policies = array( );

	/**
	 * An array of error messages.
	 *
	 * @var Array
	 */
	protected $errors;

	/**
	 * Validates the values in $values.
	 *
	 * @param array $values
	 * @return boolean
	 */
	public function validate( Array $values ) {
		$validated = true;
		foreach( $this->policies as $policy ) {
			$validated = ( true === $policy->validate( $values ) && $validated );
		}
		return $validated;
	}

	/**
	 * Adds a policy to the validator. One or more subjects may be passed.
	 *
	 * @param string $subject
	 * @return boolean
	 */
	public function add( $subject ) {
		return ( $this->policies[] = new SpValidationPolicy( $this, func_get_args( ) ) );
	}

	/**
	 * Tries to get and instantiate the rule known by the alias.
	 *
	 * @param string $alias
	 * @param array $parameters
	 * @return SpRule
	 */
	public function rule( $alias, array $parameters = array( ) ) {
		if( false === ( $rule = $this->findRule( $alias ) ) ) {
			throw new SpNoSuchRule( sprintf( 'There is no rule with the alias "%s"', $alias ) );
		}
		return new $rule( $parameters );
	}

	/**
	 * Checks if there is a rule by that alias. Returns the classname if there is, false otherwise.
	 *
	 * @param string $alias
	 * @return mixed
	 */
	protected function findRule( $alias ) {
		return isset( $this->rules[$alias] ) ? $this->rules[$alias] : false;
	}
}

/**
 * A validation policy is a combination of subjects and rules.
 *
 * @package Validation
 */
class SpValidationPolicy {
	/**
	 * Instance of the validator this policy belongs to.
	 *
	 * @var SpValidator
	 */
	protected $validator;

	/**
	 * An array of subjects, where subjects are the fields to be validated.
	 * @var Array
	 */
	protected $subjects;

	/**
	 * An array of rules which will be applied to said subjects.
	 *
	 * @var Array
	 */
	protected $rules;

	/**
	 * Construct the policy.
	 *
	 * @param SpValidator $validator
	 * @param array $subjects
	 */
	public function __construct( SpValidator $validator, array $subjects ) {
		$this->validator = $validator;
		$this->subjects = $subjects;
	}

	/**
	 * Overwriting the magic __call method. This particular instance will create
	 * known by the alias $name, and pass the arguments to the rule.
	 *
	 * @param string $name
	 * @param array $arguments
	 * @return SpValidationPolicy
	 */
	public function __call( $name, $arguments ) {
		$this->rules[] = $this->validator->rule( $name, $arguments );
		return $this;
	}

	/**
	 * Tries to validate each and every subject by applying the rules on them.
	 *
	 * @param array $values
	 * @return boolean
	 */
	public function validate( Array $values ) {
		$validates = true;
		foreach( $this->subjects as $subject ) {
			$validates = ( true === $this->rules( $this->value( $values, $subject ) ) && $validates );
		}
		return $validates;
	}

	/**
	 * Tries to find the subject in the list of values. Returns the value if it
	 * exists, or false otherwise.
	 *
	 * @param array $values
	 * @param string $subject
	 * @return mixed
	 */
	protected function value( Array $values, $subject ) {
		return isset( $values[$subject] ) ? $values[$subject] : false;
	}

	/**
	 * Applies all rules the value and returns the result.
	 *
	 * @param mixed $value
	 * @return boolean
	 */
	protected function rules( $value ) {
		$validates = true;
		foreach( $this->rules as $rule ) {
			$validates = ( true === $rule->validate( $value ) && $validates );
		}
		return $validates;
	}
}

/**
 * An abstract class all rules should implement.
 *
 * @package Validation
 */
abstract class SpRule {
	/**
	 * A list of parameters passed to the rule.
	 *
	 * @var array
	 */
	protected $parameters;

	/**
	 * Construct the rule. Do not overwrite this method unless you have very good reasons to.
	 *
	 * @param array $parameters
	 */
	public function __construct( Array $parameters = Array( ) ) {
		$this->parameters = $parameters;
	}

	/**
	 * Finds and returns a parameter at index $index, or false if it doesn't exist.
	 *
	 * @param int $index
	 * @return mixed
	 */
	protected function parameter( $index ) {
		if( !isset( $this->parameters[$index] ) ) {
			throw new SpNotEnoughParameters( sprintf( '%s expected parameter at index %d', get_class( $this ), $index ) );
		}
		return $this->parameters[$index];
	}

	/**
	 * All rules should implement this method to check whether or not a value can be validated.
	 *
	 * @param mixed $value
	 * @return boolean
	 */
	abstract public function validate( $value );
}

/**
 * Determines whether or not the value has the correct length.
 *
 * @package Validation
 */
class SpLengthBetweenRule extends SpRule {
	/**
	 * Error message that will be sent back on failure.
	 * @var string
	 */
	protected $error = 'The length of %s should be between %d and %d';

	/**
	 * Returns true if the value has the required length, false otherwise.
	 *
	 * @param mixed $value
	 * @return boolean
	 */
	public function validate( $value ) {
		return ( ( $strlen = strlen( (string) $value ) ) && ( $strlen > $this->parameter( 0 ) && $strlen < $this->parameter( 1 ) ) );
	}
}

/**
 * Determines whether or not the value is an integer, or a string with an
 * integer.
 *
 * @package Validation
 */
class SpIntegerRule extends SpRule {
	/**
	 * The error message that will be returned on failure.
	 * @var string
	 */
	protected $error = '%s is not an integer.';

	/**
	 * Determines if the value is an integer, or a string comprised of numeric characters only.
	 *
	 * @param mixed $value
	 * @return boolean
	 */
	public function validate( $value ) {
		return is_integer( $value ) || ctype_digit( $value );
	}
}

/**
 * Determines whether or not the value matches with a regular expression.
 *
 * @package Validation
 */
class SpMatchesRule extends SpRule {
	/**
	 * The error message that will be returned on failure to validate.
	 * @var string
	 */
	protected $error = '%s does not have the correct format';

	/**
	 * Returns whether or not the value matches the regular expression.
	 *
	 * @param string $value
	 * @return boolean
	 */
	public function validate( $value ) {
		return 0 !== preg_match( $this->parameter( 0 ), $value );
	}
}

/**
 * Determines whether or not passed value equals a predefined value.
 *
 * @package Validation
 */
class SpEqualRule extends SpRule {
	/**
	 * The error message that will be returned upon failure.
	 * @var string
	 */
	protected $error = '%s is not equal to %s';

	/**
	 * Returns true if the value equals the predefined value.
	 *
	 * @param mixed $value
	 * @return boolean
	 */
	public function validate( $value ) {
		if( $this->parameter( 1 ) !== false ) {
			return ( $this->parameter( 0 ) === $value );
		}
		return ( $this->parameter( 0 ) == $value );
	}
}

/**
 * Determines whether or not passed value equals a predefined value.
 *
 * @package Validation
 */
class SpNotEqualRule extends SpRule {
	/**
	 * The error message that will be returned upon failure.
	 * @var string
	 */
	protected $error = '%s is equal to %s';

	/**
	 * Returns true if the value doesn't equal the predefined value.
	 *
	 * @param mixed $value
	 * @return boolean
	 */
	public function validate( $value ) {
		if( $this->parameter( 1 ) !== false ) {
			return ( $this->parameter( 0 ) !== $value );
		}
		return ( $this->parameter( 0 ) != $value );
	}
}